Skip to content

[net11.0] Expose safe area contract for custom views - #37750

Open
kubaflo wants to merge 31 commits into
dotnet:net11.0from
kubaflo:kubaflo/37384-public-safe-area-api
Open

[net11.0] Expose safe area contract for custom views#37750
kubaflo wants to merge 31 commits into
dotnet:net11.0from
kubaflo:kubaflo/37384-public-safe-area-api

Conversation

@kubaflo

@kubaflo kubaflo commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Summary

  • make ISafeAreaElement the public per-edge safe-area contract for custom views and native hosts
  • expose HasExplicitSafeAreaEdges and GetDefaultSafeAreaEdges() so platform code can preserve control defaults and distinguish an explicit value from a default-created value
  • expose GetEffectiveSafeAreaEdges() so public native hosts read the same effective strategy as MAUI handlers, including built-in controls that intentionally preserve an explicit Default
  • expose the shared SafeAreaElement.SafeAreaEdgesProperty and SafeAreaElement.IsSafeAreaEdgesSet(BindableObject) helper for custom BindableObject implementations
  • keep the shipped ISafeAreaView contract unchanged as a compatible legacy fallback
  • keep built-in default/legacy normalization and edge-index lookup internal rather than adding another numbered public interface or a public magic-integer API
  • route iOS, Mac Catalyst, Android, keyboard, collection-cell, and nested-view safe-area handling through one capability resolver and reuse one four-edge snapshot per evaluation
  • preserve runtime Apple descendant invalidation, per-edge ancestor suppression, child keyboard overlap beyond ancestor container insets, pixel-level inset tolerance, and Android listener refresh
  • add unit, XAML, device, HostApp, AOT, mutation, and runtime-media coverage for public-only custom views and legacy behavior

This targets net11.0 because #37384 is an API request for .NET 11.

Public API and compatibility

The new public surface is added to every applicable Core and Controls API baseline:

  • ISafeAreaElement.SafeAreaEdges
  • ISafeAreaElement.HasExplicitSafeAreaEdges
  • ISafeAreaElement.GetDefaultSafeAreaEdges()
  • SafeAreaElementExtensions.GetEffectiveSafeAreaEdges(ISafeAreaElement)
  • SafeAreaElement.SafeAreaEdgesProperty
  • SafeAreaElement.IsSafeAreaEdgesSet(BindableObject)

ISafeAreaView remains unchanged because it is already shipped and implemented by external controls. Built-in controls use an internal strategy for their existing legacy/default semantics; third-party controls only implement ISafeAreaElement.

Leaving ContentPage.SafeAreaEdges unset preserves its existing platform/legacy default. Explicit values remain explicit, including full and partial Default regions, so setting the shipped named default does not become edge-to-edge. GetEffectiveSafeAreaEdges() delegates to the same strategy resolver as the handlers, preventing public native hosts from interpreting those values differently.

Validation

The broad focused build, API, unit, XAML, AOT, and platform validation below was completed through product head 777f65e227af9af1a0148a55223317a59ec94a0a. Commits c32e41e882e0b6baa9da3b830a2840262e015c2e and 310d7e93c0fd3280aba2833ef1191b54afd51a41 harden nested Bottom SoftInput residual handling and its geometry-sensitive coverage. Commit c265a307d821eea36c61116b1a549fdf7a9e03f6 adds pixel-gated overlap caching, screen-to-window keyboard conversion, active-ancestor checks, descendant convergence invalidation, a real cross-platform arrange regression, and reusable bindable-property specificity metadata. Commit 104f8146610f38465118de78bdd2c2f9336e4751 compares absolute overlap values in device-pixel buckets so cumulative sub-pixel movement cannot be ignored indefinitely. Current PR head 79cdc7408d6140335f5fa27a9aeb999897cfb0f9 adds two-dimensional floating-keyboard intersection and clamping, uses the owning window's screen scale, preserves UIKit-applied scroll insets, verifies specificity-only/no-op updates, and removes avoidable ancestor work without making suppression depend on layout order. It includes target head bedd1b18b7682193e05b47267509cec8c49c6853, preserving both the target's initial-connection guard and this PR's dynamic safe-area invalidation.

The screenshots and videos were captured at f5a35e4a729a7875b26e2474e4fd90c1a1af0099. The later commits repair Android AOT profile encoding, clarify compatibility documentation, and harden nested keyboard behavior; they do not change the captured Container → None → bottom-only Container → Container probe flow. That flow and the keyboard regressions were rebuilt and revalidated in the final platform suites.

  • exact pushed-head SafeAreaTests: 77/77
  • focused SafeAreaTests plus bindable-object selection at merged head: 174/174
  • SafeAreaEdgesTests: 15/15 across runtime, XamlC, and source-generator paths, including a third-party control using the public shared property
  • Microsoft.Maui.BuildTasks.slnf: succeeded with PublicApiType=Validate
  • Core and Controls Core: succeeded for netstandard2.0, iOS, Mac Catalyst, and Android

The platform-handler implementation was exercised at f6db02a3ad6cb67b3e555085ca03eb33997f2b98; the only later commit adds the public resolver, its documentation/API baselines, and focused tests. Exact-head platform libraries and both empirical apps were rebuilt, and the new resolver itself is displayed in the iOS and Android evidence below.

Platform Controls View Controls Page Controls ScrollView Core ScrollViewHandler
iPhone 11 Pro CoreSimulator, iOS 26.5 62/62 5/5 12 passed, 1 ignored 57 passed, 1 ignored
Mac Catalyst arm64 62/62 5/5 12 passed, 1 ignored 52 passed, 6 ignored
Android arm64 emulator (API 35 for final View) 53/53 16/16 12/12
  • exact pushed head 79cdc7408d6140335f5fa27a9aeb999897cfb0f9 was rebuilt and empirically rerun after publication: iOS 62/62, Mac Catalyst 62/62, Android 53/53, and focused units 77/77
  • FloatingKeyboardUsesClampedViewIntersection covers a laterally disjoint floating keyboard and an oversized frame; restoring the old Y-only overlap produced the sole iOS failure (61/62)
  • SystemAdjustedScrollViewInsetsAreNotSuppressedByParent preserves the full viewport inset that UIKit already applies; restoring system-inset suppression produced the sole iOS failure (61/62)
  • removing the empty-inset ancestor fast paths produced exactly the two instrumented failures (60/62)
  • restoring farther-ancestor keyboard geometry after Bottom was already resolved produced the sole iOS failure (61/62)
  • adding the suggested _safeAreaInvalidated subtree early exit produced the sole iOS failure (61/62), proving that measure-invalidated parents can still have descendants requiring safe-area invalidation
  • disabling specificity-change metadata produced the sole focused unit failure (76/77)
  • final-head nested-keyboard tests cover parent and child both declaring Bottom SoftInput: a correctly arranged child does not double-pad, while an overflowing child retains its positive frame-relative residual; transformed geometry is recomputed without requiring another keyboard notification, keyboard hide clears the residual, and a nested MauiScrollView still suppresses its raw/system Bottom inset
  • the nested arrange regression now uses a real MAUI Grid measure/arrange path instead of assigning native frames manually
  • restoring the old ancestor-SoftInput guard produced 50/51, with only the overflowing-child regression failing
  • removing active keyboard-geometry refresh produced 50/51, with the transformed child retaining stale height 50 instead of the expected 100
  • treating MauiScrollView's raw/system Bottom inset as a computed keyboard residual produced 50/51, with only the nested scroll-view suppression regression failing
  • removing the pixel-level overlap gate produced 55/56, with only the stable-geometry cache regression failing (expected 0 ancestor reads, actual 4)
  • restoring adjacent-delta comparison produced 55/56, with only the strengthened cache regression failing because two individually sub-pixel moves crossed a cumulative device-pixel boundary
  • skipping screen-to-window keyboard conversion produced 55/56, with only the nonzero-window-origin regression failing (expected X 0, actual 137)
  • treating a non-responding SoftInput ancestor under UIScrollView as active produced 55/56, with only the fallback-auto-scroll regression failing
  • removing interaction-change descendant invalidation produced 55/56, with only the non-SoftInput descendant convergence regression failing (expected height 70, actual 100)
  • disabling reusable specificity-change metadata produced Android 51/53, failing exactly the listener attach/detach regressions; the restored APK passed 53/53
  • the updated repository XHarness failed before Android app launch with Invalid userId -2; the same rebuilt APK passed 53/53 when instrumentation explicitly targeted emulator owner user 0
  • all 109 iOS HostApp SafeAreaEdges cases passed across final-head runs: both full-category runs passed 108/109, with only the order-dependent initial-state assertion after an orientation-changing fixture failing; that complete fixture immediately passed 4/4 in isolation, and the earlier exact-f5a full run passed 109/109
  • all three binary AOT profiles exactly match their text snapshots; each has two six-parameter and zero stale five-parameter OnBindablePropertySet entries
  • each profile contains one fully qualified Microsoft.Maui.SafeAreaViewStrategy type, one Android-reachable TryGetSafeAreaEdges method, no leading-dot type, and no linker-unreachable GetSafeAreaRegionsForEdge entry
  • final profile counts are maui: 38 modules / 1,814 types / 8,632 methods; maui-sc: 49 / 2,264 / 10,963; maui-blazor: 43 / 2,186 / 8,568
  • exact-head Android Mono profiled-AOT publish consumed the profiles and compiled all 110/110 assemblies; the unstripped output contains the native Microsoft_Maui_SafeAreaViewStrategy_TryGetSafeAreaEdges_object_Microsoft_Maui_SafeAreaEdges__bool body
  • temporary public-only Sandbox probe built in Release for iOS Simulator and Android arm64 before the empirical runs below
  • mutation checks proved the assertions detect public custom-view resolution, canonical public-host strategy precedence, declared defaults, explicit and partial ContentPage.Default, specificity-only assignments, Android exact inset consumption, nested/disjoint Apple edges, child keyboard overlap beyond a parent container inset, complete descendant invalidation, legacy MauiScrollView behavior, residual sub-pixel ancestor insets, completed ancestor traversal, and stale five-parameter AOT signatures

Completed public build 1567759 uses merge commit 30ce3756e59cf4d10e60ea9c6ce9f5e916746767, whose second parent is PR head 490a33f8dc17a653823d181f844aacd41b2e87fc. Every integration-test job succeeded, including Android runtime, six iOS CoreCLR/NativeAOT configurations, AOT/build, samples, Blazor, multi-project, and Windows template/build coverage.

Replacement build 1567853 uses merge commit cdb262e0517e81025f2038574b3a1bd9da829515, with target parent 4695c95801e0b6764beb83f314c62141ee9c7f2e and PR parent 310d7e93c0fd3280aba2833ef1191b54afd51a41. Its final raw timeline contains 24 successful jobs and only two failed jobs: Windows Debug and Release. Every failed task/job/phase log was downloaded and inspected without deduplication; both build tasks report only the target branch's four CS0103 errors for missing AssertEventually at TabbedPageTests.Windows.cs lines 141 and 155 across the two Windows TFMs. Both Windows Helix jobs succeeded. Target commit c127c3f3503e06a347c0f100504911659f6154c1 fixes that compile error and is included in current head 79cdc7408d6140335f5fa27a9aeb999897cfb0f9.

Build 1568885 was audited from every failed raw phase/job/task log and all 16 Helix work-item details and console logs, preserving repeated lines. Every test process passed with zero failures and exited 0; Windows Helix reported exit -4 only when publishing results failed with TF10216: Azure DevOps services are currently unavailable or one Azure request-read timeout. This build contains no product or test failure.

Claude Opus 5 independently reviewed the safe-area hot paths and rejected both applied-state ancestor caching and _safeAreaInvalidated subtree pruning as layout-order correctness regressions. Its topology-independent optimizations are in 79cdc7408d. A separate GPT-5.6 Terra exact-diff review reported no significant issues. All six exact-head review threads have evidence-backed replies and are resolved.

Empirical evidence

A temporary SafeAreaProbe : TemplatedView, ISafeAreaElement implemented only the new public contract, reused the public shared bindable property, and displayed GetEffectiveSafeAreaEdges() as Public host: .... At the capture head it was advanced live through all-edge Container, None, bottom-only Container, and back to all-edge Container. The probe was removed after capture.

Both videos are H.264/yuv420p at a constant 30 fps and contain only the ordered runtime states 1 → 2 → 3 → 1; sampled-frame review found no blank, splash, crash, or stale-state frames. On iOS, Appium taps drove the live property changes deterministically.

iOS — iPhone 11 Pro simulator

All edges (T44/B34) Edge-to-edge (T0/B0) Bottom only (T0/B34)
iOS public custom view using Container on all edges iOS public custom view using None edge-to-edge iOS public custom view using Container on the bottom edge only

Video — live runtime transitions:

ios-safe-area-demo-f5a.mp4

Android — API 34 arm64 emulator

The probe theme leaves the Android system bars opaque. Edge-to-edge is demonstrated by the SAFE CONTENT markers moving beneath those bars (and therefore disappearing), while bottom-only restores only the bottom marker; the background cannot show through the opaque bars.

All edges Edge-to-edge Bottom only
Android public custom view using Container on all edges Android public custom view using None edge-to-edge Android public custom view using Container on the bottom edge only

Video — live runtime transitions:

android-safe-area-demo-f5a.mp4

MauiBot follow-up

  • applied the connect-time Apple invalidation guard
  • strengthened Android coverage to assert exact consumed insets and listener replacement
  • simplified the final public surface to one modern custom-view contract while retaining the existing legacy interface unchanged
  • documented custom-view Default resolution, explicit shared-property misuse, and the public CLR-property pattern required by XAML
  • preserved explicit and partially explicit ContentPage.Default regions while retaining the unset edge-to-edge default
  • added one canonical public-host resolver so public consumers and handlers preserve those built-in defaults identically
  • replaced repeated handler/type resolution on layout paths with one reusable four-edge snapshot
  • made nested Apple suppression per-edge, pixel-tolerant, and bounded once all four ancestor-handled edges are known
  • preserved a child's frame-relative keyboard overlap when an ancestor handles only the smaller container inset, with an arranging-parent regression whose assertion fails under unconditional suppression
  • made every Bottom SoftInput MauiView compute its own residual even when an ancestor also declares SoftInput: an arranged child falls back to ordinary ancestor suppression, while an overflowing child retains only its positive live overlap
  • refreshed active Bottom SoftInput geometry during layout so transforms cannot preserve stale keyboard overlap; mutation testing makes the transformed-child assertion fail without the refresh
  • retained ancestor-edge caches across unchanged keyboard-visible layout passes by comparing absolute overlap values in device-pixel buckets, including cumulative sub-pixel movement
  • converted keyboard notification frames from screen coordinates into the active window before view-frame intersection
  • required a two-dimensional keyboard/view intersection, clamped overlap to the view height, and classified ancestor pixel values with the owning window's screen scale
  • required a SoftInput ancestor to respond to safe area before it can suppress keyboard auto-scroll
  • propagated actual safe-area interaction changes to plain descendants so multi-pass keyboard layout converges without per-layout subtree churn
  • exercised the synchronous cross-platform arrange ordering through a real MAUI Grid
  • replaced the shared Element safe-area identity special case with reusable internal bindable-property specificity metadata
  • documented the same-edge keyboard-residual exception and why MauiScrollView deliberately keeps ordinary suppression for raw/system insets, with a bottom-edge regression that fails if the keyboard exemption is applied there
  • preserved UIKit SystemAdjustedContentInset values while keeping per-edge ancestor suppression for manually computed scroll insets
  • skipped ancestor lookup for empty adjusted insets and skipped farther-ancestor keyboard geometry once Bottom is resolved
  • retained full native-subtree invalidation because an exact mutation proves _safeAreaInvalidated cannot represent descendant state; a persistent latch is also unsound across same-window reparenting of generic UIView subtrees
  • preserved the pre-existing edge-to-edge fallback for legacy-only IScrollView implementations
  • regenerated all three binary/text AOT profile pairs for the six-parameter property-set signatures, repaired the fully qualified strategy type, removed the Android-unreachable strategy method, and validated them with a real Mono profiled-AOT publish

Existing PR comparison

I searched the open pull requests for #37384 and equivalent safe-area API titles. No competing implementation exists, so there was no alternative change set to compare.

Fixes #37384

Make the per-edge safe area interfaces and shared bindable-property plumbing reusable outside MAUI. Keep platform inset reporting internal, migrate built-in controls, and add custom-view regression coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings August 22, 2026 23:18
@kubaflo
kubaflo temporarily deployed to copilot-pat-pool August 22, 2026 23:19 — with GitHub Actions Inactive
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 37750

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 37750"

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

@kubaflo
kubaflo temporarily deployed to copilot-pat-pool August 22, 2026 23:19 — with GitHub Actions Inactive
@kubaflo
kubaflo temporarily deployed to copilot-pat-pool August 22, 2026 23:20 — with GitHub Actions Inactive
@kubaflo
kubaflo temporarily deployed to copilot-pat-pool August 22, 2026 23:23 — with GitHub Actions Inactive
@kubaflo
kubaflo temporarily deployed to copilot-pat-pool August 22, 2026 23:24 — with GitHub Actions Inactive
@github-actions github-actions Bot added area-safearea Issues/PRs that have to do with the SafeArea functionality platform/ios labels Aug 22, 2026
@kubaflo
kubaflo temporarily deployed to copilot-pat-pool August 22, 2026 23:24 — with GitHub Actions Inactive

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR exposes public per-edge safe-area contracts for custom MAUI views and native hosts, adds shared plumbing, and preserves internal iOS inset reporting.

Changes:

  • Publishes safe-area interfaces and edge lookup APIs.
  • Migrates built-in controls to shared safe-area helpers.
  • Adds API baselines and regression coverage.

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 1 comment.

Show a summary per file
File Summary
src/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt Records Core API additions.
src/Core/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt Records Core API additions.
src/Core/src/PublicAPI/net/PublicAPI.Unshipped.txt Records Core API additions.
src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt Records Core API additions.
src/Core/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt Records Core API additions.
src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt Records Core API additions.
src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt Records Core API additions.
src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt Records Core API additions.
src/Core/src/Primitives/SafeAreaEdges.cs Exposes per-edge lookup.
src/Core/src/Platform/iOS/MauiView.cs Uses the internal inset sink.
src/Core/src/Core/ISafeAreaView2.cs Publishes the per-edge safe-area contract.
src/Core/src/Core/ISafeAreaInsets.cs Defines internal inset reporting.
src/Core/src/Core/ISafeAreaElement.cs Publishes the shared element contract.
src/Controls/tests/Core.UnitTests/SafeAreaTests.cs Critical (1 vote): the private CustomSafeAreaView has a non-public constructor, causing Activator.CreateInstance(Type) to throw MissingMethodException.
src/Controls/src/Core/ScrollView/ScrollView.cs Migrates safe-area behavior.
src/Controls/src/Core/SafeAreaElement.cs Adds shared safe-area property plumbing.
src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt Records Controls API additions.
src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt Records Controls API additions.
src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt Records Controls API additions.
src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt Records Controls API additions.
src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt Records Controls API additions.
src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt Records Controls API additions.
src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt Records Controls API additions.
src/Controls/src/Core/Page/Page.cs Implements inset handling.
src/Controls/src/Core/Layout/Layout.cs Migrates safe-area behavior.
src/Controls/src/Core/ContentView/ContentView.cs Migrates safe-area behavior.
src/Controls/src/Core/ContentPage/ContentPage.cs Migrates safe-area behavior.
src/Controls/src/Core/Border/Border.cs Migrates safe-area behavior.
Suppressed comments (4)

src/Controls/src/Core/ContentPage/ContentPage.cs:174

  • HasExplicitSafeAreaEdges now correctly uses IsSafeAreaEdgesSet, but the adjacent per-edge resolver still uses IsSet. IsSet treats default-value creation as set, so merely reading SafeAreaEdges first causes this method to skip the iOS IgnoreSafeArea fallback and return the default None instead. Use SafeAreaElement.IsSafeAreaEdgesSet(this) here as well and cover the read-then-resolve sequence.
		bool ISafeAreaView2.HasExplicitSafeAreaEdges => SafeAreaElement.IsSafeAreaEdgesSet(this);

src/Controls/src/Core/Layout/Layout.cs:376

  • Please regenerate the profiled AOT artifacts for this rename. The checked-in maui.aotprofile.txt and maui-sc.aotprofile.txt still list Layout/ScrollView:ISafeAreaElement.SafeAreaEdgesDefaultValueCreator, while these implementations are now GetDefaultSafeAreaEdges; Microsoft.Maui.Controls.targets imports the corresponding binary profiles for Android, so these calls will no longer match the profiled methods (and the profile tool may report missing methods).
		SafeAreaEdges ISafeAreaElement.GetDefaultSafeAreaEdges()
		{
			return SafeAreaEdges.Container;
		}

src/Controls/src/Core/ScrollView/ScrollView.cs:553

  • This renames the explicit ISafeAreaElement implementation used by Layout and ScrollView, but the checked-in profiled-AOT lists still reference Microsoft.Maui.ISafeAreaElement.SafeAreaEdgesDefaultValueCreator (in maui.aotprofile.txt and maui-sc.aotprofile.txt). Regenerate those profile outputs so they reference GetDefaultSafeAreaEdges; otherwise the profiles are stale and no longer describe methods in the assembly.
		SafeAreaEdges ISafeAreaElement.GetDefaultSafeAreaEdges()

src/Controls/tests/Core.UnitTests/SafeAreaTests.cs:287

  • These assertions verify the public contracts and property mapping only; they never create a handler or send insets through the iOS MauiView/Android inset-listener paths. A regression in the platform-side ISafeAreaView2 lookup or listener refresh would therefore still pass these tests even though a direct custom View no longer receives the advertised per-edge behavior. Add focused device coverage for the custom view on Android and iOS, or include the probe as an automated test.
		public void CustomView_CanReuseSafeAreaEdgesContract()
		{
			var view = new CustomSafeAreaView();
			var safeAreaView = (ISafeAreaView2)view;

Comment thread src/Controls/tests/Core.UnitTests/SafeAreaTests.cs Outdated
@kubaflo

kubaflo commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

Validated the review findings against the current head:

  • ContentPage.GetSafeAreaRegionsForEdge still uses IsSet, so default-value creation can incorrectly bypass the legacy iOS fallback.
  • Both checked-in AOT profiles retain four SafeAreaEdgesDefaultValueCreator entries after the rename.
  • The 28-file diff contains no device-test coverage for the platform inset path.
  • The private-constructor Activator.CreateInstance issue is confirmed separately in the inline thread.

This branch is actively owned in another worktree with recent source/build activity, so the monitor is reply-only here: I did not edit or push, and the actionable thread remains open for the owning agent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
Copilot AI review requested due to automatic review settings August 23, 2026 02:09
@kubaflo

kubaflo commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

@copilot-pull-request-reviewer addressed the validated feedback in ecf8393: corrected ContentPage explicit-edge detection after default-value reads, regenerated both binary/text AOT profiles for the renamed explicit implementations, fixed private test-view construction, and added Android/iOS custom-view handler coverage. Focused validation passed (64 unit, 40 iOS View, 5 iOS Page, and 51 Android View tests). This is ready for re-review — thanks!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 36 changed files in this pull request and generated 2 comments.

Suppressed comments (1)

src/Controls/src/Core/SafeAreaElement.cs:24

  • This newly reusable property is not wired to safe-area invalidation on Mac Catalyst: the existing ViewHandler mapper is guarded by #if ANDROID || IOS, while MauiView and ViewHandler.iOS.cs also compile for Mac Catalyst. Consequently, changing a custom view's SafeAreaEdges after its handler is connected does not invalidate the safe-area layout there. Register MapSafeAreaEdges for MACCATALYST as well and add a post-handler-change regression test.
		public static readonly BindableProperty SafeAreaEdgesProperty =
			BindableProperty.Create(nameof(ISafeAreaElement.SafeAreaEdges), typeof(SafeAreaEdges), typeof(ISafeAreaElement), SafeAreaEdges.Default,
									defaultValueCreator: SafeAreaEdgesDefaultValueCreator);

Comment thread src/Controls/src/Core/SafeAreaElement.cs Outdated
Comment thread src/Core/src/Core/ISafeAreaView2.cs Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
Copilot AI review requested due to automatic review settings August 23, 2026 03:36
@kubaflo

kubaflo commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

@copilot-pull-request-reviewer[bot] addressed both latest findings in dc737cb: descendant safe-area caches are invalidated when an ancestor strategy changes, and the mapper now runs on Mac Catalyst. Focused View device tests passed 41/41 on both iOS and Mac Catalyst. This is ready for re-review — thanks!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 36 out of 38 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-sc.aotprofile.txt:3287

  • The text snapshot now records GetDefaultSafeAreaEdges, but the paired maui-sc.aotprofile binary is not regenerated. Microsoft.Maui.Controls.targets imports that binary for Android profiled AOT, so consumers will still ship a profile containing the removed interface method and will not profile the new calls. Please rerun the documented Record target for maui-sc and commit the regenerated binary with this snapshot.
    src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui.aotprofile.txt:2594
  • The text snapshot now records GetDefaultSafeAreaEdges, but the paired maui.aotprofile binary is not regenerated. Microsoft.Maui.Controls.targets imports that binary for Android profiled AOT, so consumers will still ship a profile containing the removed interface method and will not profile the new calls. Please rerun the documented Record target for maui and commit the regenerated binary with this snapshot.
	Microsoft.Maui.SafeAreaEdges Microsoft.Maui.Controls.Layout:Microsoft.Maui.ISafeAreaElement.GetDefaultSafeAreaEdges ()

@kubaflo

kubaflo commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the re-review. The two suppressed AOT-profile notes are already satisfied by ecf8393: that commit changed both maui.aotprofile and maui-sc.aotprofile, and aprofutil -m shows the new GetDefaultSafeAreaEdges entries in each binary. A full sorted method-set comparison against the checked-in text snapshots reports zero differences (maui: 8,634 methods; maui-sc: 10,965 methods), so another profile regeneration would be redundant. No code change is needed for these suppressed findings.

@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Aug 23, 2026
@MauiBot

This comment has been minimized.

@kubaflo

kubaflo commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Additional exact-head hardening is pushed at 104f8146610f38465118de78bdd2c2f9336e4751.

The keyboard-overlap cache now compares absolute values after device-pixel rounding. The prior adjacent-delta comparison could update its baseline after every sub-pixel move and therefore miss a sequence whose cumulative motion crossed a physical pixel boundary.

The strengthened cache regression first proves an unchanged/sub-pixel pass performs zero ancestor reads, then advances by two 0.4-pixel steps and proves the cumulative 0.8-pixel move invalidates. Restored iOS Simulator and Mac Catalyst suites pass 56/56; reverting only the absolute-bucket comparison produces 55/56, with UnchangedKeyboardGeometryKeepsAncestorSafeAreaCache as the sole failure. The iOS mutation and restored run were repeated on an isolated CoreSimulator to exclude concurrent bundle interference.

@kubaflo

This comment has been minimized.

@PureWeen PureWeen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review

An independent reviewer process reviewed the full GitHub-scoped diff and exact source at 104f8146610f38465118de78bdd2c2f9336e4751.

Finding

One exact-diff iOS regression survived adversarial consensus: a floating or undocked keyboard can produce false SoftInput bottom padding for a laterally disjoint view. See the inline comment for the concrete path and fix. Consensus: 2/3 reviewers after dispute.

Prior review reconciliation

The wrong secondary-display scale and full-subtree invalidation costs were independently rediscovered, but MauiBot already documented them in issue comment 5420344625; they are not duplicated here. Earlier findings about coordinate conversion, active ancestors, descendant convergence, explicit defaults, and specificity propagation are addressed in the current code.

Finalization

The title accurately describes the new public Safe Area contract, and the detailed description matches the implementation, compatibility model, API baselines, AOT profiles, and test scope.

Methodology

3 independent reviewers with adversarial consensus + a separate MAUI domain specialist. Review event: COMMENT; no approval or change request is implied.

Comment thread src/Core/src/Platform/iOS/MauiView.cs Outdated

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expert Review — 5 findings

See inline comments for details.

Comment thread src/Core/src/Platform/iOS/MauiView.cs
Comment thread src/Core/src/Platform/iOS/MauiView.cs
Comment thread src/Core/src/Platform/iOS/MauiView.cs Outdated
Comment thread src/Core/src/Platform/iOS/MauiScrollView.cs Outdated
Comment thread src/Controls/src/Core/BindableProperty.cs
MauiBot

This comment was marked as outdated.

@MauiBot MauiBot added s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates and removed s/agent-fix-win AI found a better alternative fix than the PR s/agent-review-in-progress AI review is currently running for this PR labels Aug 26, 2026
Handle floating keyboard geometry, preserve UIKit-adjusted scroll insets, and avoid redundant ancestor work while retaining layout-order correctness.

Add focused device and specificity regressions for every exact-head review finding.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: de9c7c01-82c4-42fd-9ab7-882279aadd15
@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Aug 26, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: de9c7c01-82c4-42fd-9ab7-882279aadd15
@MauiBot

This comment has been minimized.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Aug 27, 2026
@kubaflo

kubaflo commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

The AI summary is based on stale head 79cdc74, as its own warning notes. Current head 3d3c42b incorporates the review follow-ups for system-adjusted nested scroll insets, the hidden-keyboard fast path, API-contract documentation, and the Android connecting-handler guard. This branch is actively owned and running the targeted keyboard/UI validation now, so I am leaving that live validation flow undisturbed; a current-head review result can supersede this informational report.

@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Aug 27, 2026
@kubaflo

kubaflo commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

/azp run maui-pr

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Copilot CI and others added 2 commits August 27, 2026 06:40
Clarify the intentional UIKit and keyboard semantics, document compatibility-resolved effective values, and remove unrelated Android event-hook syntax churn.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: de9c7c01-82c4-42fd-9ab7-882279aadd15
Explain why interface specificity is required, why native subtree invalidation cannot stop at intermediate MAUI views, how nested keyboard residuals settle, and why the Page strategy supports custom subclasses.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: de9c7c01-82c4-42fd-9ab7-882279aadd15
@MauiBot

This comment has been minimized.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Aug 27, 2026
@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Aug 27, 2026

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expert Review — 12 findings

See inline comments for details.


var oldApplyingSafeAreaAdjustments = _appliesSafeAreaAdjustments;
_appliesSafeAreaAdjustments = !IsParentHandlingSafeArea() && RespondsToSafeArea() && !_safeArea.IsEmpty;
_appliesSafeAreaAdjustments = RespondsToSafeArea() && !_safeArea.IsEmpty;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 AI-Generated Review (multi-model)

❌ Error — [major] Layout Measure-Arrange / Regression Prevention: the !IsParentHandlingSafeArea() term was dropped from _appliesSafeAreaAdjustments, but ancestor suppression was only re-added to the first branch above (SystemAdjustedContentInset == Zero || ContentInsetAdjustmentBehavior == Never, where ExcludeParentHandledSafeAreaEdges is applied). In the else branch _safeArea = SystemAdjustedContentInset.ToSafeAreaInsets() is assigned with no parent filtering at all, so _appliesSafeAreaAdjustments is now true whenever UIKit reports a non-zero adjusted inset — even when an ancestor MauiView is already padding the same edges.

Concrete scenario: ContentPage SafeAreaEdges="All" (or Container) → VerticalStackLayoutScrollView whose content is taller than the viewport (so ContentInsetAdjustmentBehavior stays Automatic/Always and SystemAdjustedContentInset != Zero). Before this change the parent-handling check forced _appliesSafeAreaAdjustments = false for that scroll view; now both the ancestor MauiView and the MauiScrollView inset the same top/bottom edges. This is exactly the double-apply/oscillation shape tracked by #33595 and #32586, and the ExcludeParentHandledSafeAreaEdges guard that replaces it is unreachable on this branch. Either apply the same ancestor-edge exclusion to the SystemAdjustedContentInset result, or restore an IsParentHandlingSafeArea-style gate for that branch. ScrollViewHandlerTests.iOS.cs only adds LegacySafeAreaViewWithoutModernContractRemainsEdgeToEdge, which exercises the Never branch, so this path has no regression coverage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I am keeping UIKit-adjusted insets unsuppressed: AdjustedContentInset is already physically applied by UIKit, and _safeArea compensates the MAUI viewport for that native inset; filtering it through ancestor padding discards part of the real scroll viewport. SystemAdjustedScrollViewInsetsAreNotSuppressedByParent covers this exact branch, and the inverse suppression mutation was the sole failure in the 62-test iOS View suite. The manually computed/Never branch still applies per-edge ancestor suppression.

// depend on whether the ancestor has already completed its layout pass.
// Keyboard overlap only changes Bottom, so farther ancestors can omit that
// calculation after the nearest nonzero Bottom has been classified.
var safeArea = mauiView.GetAdjustedSafeAreaInsets(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 AI-Generated Review (multi-model)

⚠️ Warning — [major] Performance-Critical Path: GetParentHandledSafeAreaEdges now recomputes each ancestor's full adjusted safe area (mauiView.GetAdjustedSafeAreaInsets(...)) while walking the superview chain. The previous implementation read the already-computed mv._appliesSafeAreaAdjustments bool field. Each call now performs SafeAreaViewStrategy.TryGetSafeAreaEdges (interface type tests + a virtual GetDefaultSafeAreaEdges() call + SafeAreaEdges struct construction), a 4-iteration region scan, and — for the nearest ancestor with a zero bottom — TryGetSoftInputBottomOverlap with ConvertRectToView and ConvertRectFromCoordinateSpace.

This runs per MauiView per layout pass, making the cost O(views × depth). It is cached in _parentHandledSafeAreaEdges, but the new InvalidateSafeArea(UIView) / InvalidateDescendantSafeAreas() recursion (lines 863 and 976) nulls that cache for every descendant on each safe-area change, so the cache is discarded exactly when the tree is being re-laid-out. On a page containing a CollectionView with many realized MauiView-backed cells this is a measurable per-keyboard-event and per-rotation regression. Please attach a dotnet-trace comparison against the base commit, or hoist the ancestor computation so a single upward walk populates all descendants.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I am keeping the input-based ancestor lookup because reading an ancestor cached applied-state flag makes child suppression depend on ancestor layout order; ParentSafeAreaSuppressionDoesNotDependOnLayoutOrder covers that regression. The result is cached per descendant until a real invalidation, empty insets skip the lookup, the walk stops when all four edges resolve, and keyboard geometry is omitted for farther ancestors once Bottom resolves. Those bounds have focused device coverage; there is no measured regression supporting a correctness-reducing cache.

/// <summary>
/// Invalidates safe area state for a native subtree.
/// </summary>
internal static void InvalidateSafeArea(UIView platformView)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 AI-Generated Review (multi-model)

⚠️ Warning — [major] Performance-Critical Path / iOS Platform: InvalidateSafeArea(UIView) recurses into the entire native subtree unconditionally, with no depth bound and no early exit for branches that contain no safe-area-aware views. Every node access of platformView.Subviews marshals a fresh managed UIView[] out of the underlying NSArray, so a page with N native descendants allocates N arrays per invalidation.

This is invoked from ClearKeyboardState(), KeyboardWillShow, KeyboardWillChangeFrame, and ViewHandler.iOS.MapSafeAreaEdges — i.e. on every keyboard show/hide/frame change. With a CollectionView of realized cells (each cell being a MauiView subtree), one keyboard toggle now walks and allocates across all of them. Consider bounding the walk (stop descending once a subtree contains no MauiView/MauiScrollView), or reusing a cached descendant list.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. The complete native-subtree walk is intentional because handler-backed descendants can sit below arbitrary UIKit wrappers, so there is no sound type-based signal that a branch contains no MauiView/MauiScrollView. A farther Top-handling ancestor can still affect a grandchild below an intermediate Bottom-handling view. Stopping below that intermediate view was mutation-tested and failed only ChangingAncestorSafeAreaEdgesInvalidatesEdgeDisjointGrandchild; restoring the walk passed. Connect-time mapping already skips this traversal, and runtime broadcasts are required for keyboard/property invalidations.

(oldSafeArea.EqualsAtPixelLevel(_safeArea) || !_appliesSafeAreaAdjustments);

if (!interactionUnchanged)
InvalidateDescendantSafeAreas();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 AI-Generated Review (multi-model)

⚠️ Warning — [major] Layout Measure-Arrange: InvalidateDescendantSafeAreas() is called from inside ValidateSafeArea(), which itself runs on the layout path (AdjustForSafeArea/LayoutSubviews). Calling SetNeedsLayout() on every descendant during a layout pass schedules another full pass, and each descendant that changes then schedules a pass for its descendants.

Combined with the intentional two-phase keyboard convergence introduced at line 592 (child keeps its own positive overlap until the parent arranges it above the keyboard), a single keyboard show on a SoftInput-bottom page containing a SoftInput-bottom child costs at minimum: pass 1 (parent pads, child also pads) → pass 2 (child overlap now 0, child suppresses) → pass 3 (parent re-measures the shrunken child). If the child's arranged position depends on the inset it applies — e.g. a VerticalStackLayout with SafeAreaEdges="SoftInput" inside a ScrollView whose content height feeds back into the parent's arrange — the overlap never reaches a fixed point and the passes oscillate. The EqualsAtPixelLevel guard only terminates the loop when the values converge; it does not prevent a non-converging feedback cycle. Please confirm with a device test that measures layout-pass count across a keyboard show for nested SoftInput edges.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. InvalidateDescendantSafeAreas runs only when the parent safe-area interaction actually changes, not on every validation/layout pass, and UIKit coalesces SetNeedsLayout. NestedKeyboardSafeAreasUseCrossPlatformArrange exercises the real MAUI Grid measure/arrange path, while ParentAndChildKeyboardSafeAreasDoNotDoublePadArrangedChild and ParentAndChildKeyboardSafeAreasProtectOverflowingChild cover both convergence outcomes. There is no reproducing oscillation here, and a scheduler-specific pass-count assertion would be brittle, so I am keeping the behavior.

parentHandledEdges.Left != SafeAreaRegions.None ? 0 : safeArea.Left,
parentHandledEdges.Right != SafeAreaRegions.None ? 0 : safeArea.Right,
parentHandledEdges.Top != SafeAreaRegions.None ? 0 : safeArea.Top,
!bottomIncludesKeyboardOverlap && parentHandledEdges.Bottom != SafeAreaRegions.None ? 0 : safeArea.Bottom);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 AI-Generated Review (multi-model)

⚠️ Warning — [major] Safe Area and Window Insets: ExcludeParentHandledSafeAreaEdges skips bottom-edge suppression whenever bottomIncludesKeyboardOverlap is true, which removes the !IsSoftInputHandledByParent(this) gate that previously prevented an ancestor and a descendant from both padding for the keyboard. The comment states the child's overlap becomes zero "once its parent arranges that child above the keyboard" — but that assumption fails when the parent's inset does not move the child.

Concrete scenario: a Grid page with SafeAreaEdges="SoftInput" whose bottom row child also sets SafeAreaEdges="SoftInput" and is VerticalOptions="End" inside a fixed-height row, or a child inside an absolutely-positioned container. The parent's bottom padding shrinks the parent's content rect but the child's window-space bottom is unchanged, so TryGetSoftInputBottomOverlap keeps returning a positive overlap forever and the keyboard padding is applied twice (content pushed up by roughly 2× the keyboard height). At minimum this needs a device test covering a nested SoftInput child whose frame is not repositioned by the parent's inset.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That fixed-position case is the reason the positive residual is retained. If parent padding does not move the child, the parent has not protected that child from the keyboard; suppressing the child overlap would under-pad it, not prevent a duplicate movement. The suite separately proves that an arranged child reaches zero residual and does not double-pad, while an overflowing/fixed child keeps only its own frame-relative overlap. Restoring the old parent SoftInput gate fails the overflowing-child regression, so I am keeping this logic.


if (handler.PlatformView is PlatformView platformView)
{
MauiView.InvalidateSafeArea(platformView);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 AI-Generated Review (multi-model)

⚠️ Warning — [moderate] Handler Mapper and Property Patterns / Performance: MapSafeAreaEdges now performs a full recursive native-subtree invalidation (MauiView.InvalidateSafeArea(platformView)) on every mapper invocation. Because SafeAreaElement.SafeAreaEdgesProperty sets UpdateHandlerOnSpecificityChange = true (SafeAreaElement.cs:36) and Element.OnBindablePropertySet now forwards specificity-only transitions as changed: true (Element.cs:711), this mapper also fires when the value is unchanged and only the setter specificity moved — e.g. an implicit Style re-applying SafeAreaEdges="None" over the created default, or a VisualState re-entering the same state.

The result is a whole-subtree walk (including realized CollectionView cells) for a no-op value change. Consider comparing the newly resolved effective edges against the previously applied ones and returning early when they are pixel-identical, or scoping the invalidation to the handler's own platform view plus safe-area-aware descendants.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A specificity-only transition is semantically meaningful here: the raw SafeAreaEdges value can stay equal while HasExplicitSafeAreaEdges changes and therefore changes the resolved default strategy. SafeAreaEdgesSpecificityChangesUpdateHandlerOnlyOnce verifies that a transition updates exactly once and a repeated same-specificity assignment does not update. Caching only this handler applied value would also miss descendants whose ancestor-suppression result changes, which is why the correctness-required descendant invalidation remains.

Comment thread src/Controls/src/Core/Element/Element.cs
/// The type must use <see cref="SafeAreaEdgesProperty"/> as the backing store for its safe area property.
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="bindable"/> is <see langword="null"/>.</exception>
public static bool IsSafeAreaEdgesSet(BindableObject bindable)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 AI-Generated Review (multi-model)

💡 Suggestion — [minor] Public API Surface: IsSafeAreaEdgesSet(BindableObject) is now public but silently returns false for any BindableObject that declares its own SafeAreaEdges bindable property instead of reusing this shared SafeAreaEdgesProperty instance. The remark documents the requirement, but the failure mode is silent: a custom control that follows the ISafeAreaElement guidance yet declares BindableProperty.Create(nameof(SafeAreaEdges), ...) locally will report "never explicitly set", so SafeAreaViewStrategy resolves it via GetDefaultSafeAreaEdges() and the user's assigned value is ignored on platforms that branch on explicitness.

Given this ships as a public extensibility contract, consider making the mismatch detectable — e.g. having IsSafeAreaEdgesSet throw (or Debug.Assert) when bindable.GetType() does not expose SafeAreaElement.SafeAreaEdgesProperty as its SafeAreaEdgesProperty — rather than silently degrading.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The helper contract deliberately requires reuse of the shared property, as its remarks and the interface guidance state. Reflection over a public static field would be trimming-sensitive and still could not prove which bindable property the instance accessor actually uses. A custom implementation with its own property must compute HasExplicitSafeAreaEdges itself; implementations choosing the shared helper get one canonical identity, and the shared default creator already throws when used by a non-ISafeAreaElement. I am keeping that explicit, predictable contract.

Comment thread src/Core/src/Core/ISafeAreaElement.cs
Comment thread src/Controls/src/Core/Page/Page.cs

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review Summary

@kubaflo — new AI review results are available based on commit 92f72f9.

Gate Inconclusive Confidence High Platform iOS


🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix

Gate Result: ⚠️ INCONCLUSIVE

Platform: IOS · Base: net11.0 · Merge base: ec79089f

🩺 Could not verify — environment/infrastructure error. The gate ran the tests but hit an environment error (an emulator/simulator/Appium/XHarness flake, a device that would not boot, or an empty/invalid result file), so it could not record a real pass/fail. The ⚠️ ENV ERROR marks below are infrastructure, not test failures — this is not a problem with your PR. Comment /review to retry on a fresh agent.

XHarness did not produce the expected fresh result 'testResults.xml' for requested class(es) 'Microsoft.Maui.DeviceTests.ScrollViewHandlerTests' (the target tests did not run).

⚠️ Gate coverage limitations

  • The A/B gate did not verify 1 dropped DeviceTest group(s): ViewTests (CustomViewSafeAreaEdgesReachMauiView, ChangingParentSafeAreaEdgesInvalidatesDescendants, MeasureInvalidatedParentDoesNotBlockDescendantSafeAreaInvalidation, ParentSafeAreaSuppressionDoesNotDependOnLayoutOrder, ResolvedBottomEdgeSkipsFartherAncestorKeyboardGeometry, ResidualParentInsetDoesNotSuppressChildSafeArea, ParentHandledEdgeLookupStopsWhenAllEdgesAreResolved, EmptySafeAreaSkipsParentHandledEdgeLookup, EmptyManualScrollViewSafeAreaSkipsParentHandledEdgeLookup, KeyboardSafeAreaChangesInvalidateDescendants, ParentContainerSafeAreaDoesNotSuppressChildKeyboardSafeArea, ParentAndChildKeyboardSafeAreasDoNotDoublePadArrangedChild, ParentAndChildKeyboardSafeAreasProtectOverflowingChild, HiddenKeyboardSkipsDuplicateSoftInputStrategyResolution, UnchangedKeyboardGeometryKeepsAncestorSafeAreaCache, ChangedKeyboardOverlapInvalidatesNonSoftInputDescendants, SoftInputAncestorInsideScrollViewDoesNotSuppressKeyboardAutoScroll, KeyboardFrameConvertsFromScreenToWindowCoordinates, FloatingKeyboardUsesClampedViewIntersection, NestedKeyboardSafeAreasUseCrossPlatformArrange, ParentOnlySuppressesOverlappingChildSafeAreaEdges, ParentOnlySuppressesOverlappingScrollViewSafeAreaEdges, SystemAdjustedScrollViewInsetsAreNotSuppressedByParent, ChangingAncestorSafeAreaEdgesInvalidatesEdgeDisjointGrandchild). Deep UI Tests runs HostApp UI categories only and does not execute DeviceTests; separate device-test validation is required.
Test Without Fix (expect FAIL) With Fix (expect PASS)
🧪 SafeAreaTests SafeAreaTests 🛠️ BUILD ERROR ✅ PASS — 15s
📄 SafeAreaEdgesTests SafeAreaEdgesTests 🛠️ BUILD ERROR ✅ PASS — 19s
📄 Tests Tests 🛠️ BUILD ERROR ✅ PASS — 120s
📱 PageTests (ReadingDefaultSafeAreaEdgesPreservesLegacySafeAreaFallback) Category=Page 🛠️ BUILD ERROR ⚠️ ENV ERROR
📱 ScrollViewHandlerTests (LegacySafeAreaViewWithoutModernContractRemainsEdgeToEdge) Category=ScrollView ⚠️ ENV ERROR ⚠️ ENV ERROR
🔴 Without fix — 🧪 SafeAreaTests: 🛠️ BUILD ERROR · 15s

Error-relevant lines (filtered from the build log):

/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(577,35): error CS0539: 'SafeAreaTests.DerivedSafeAreaContentPage.GetDefaultSafeAreaEdges()' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(583,26): error CS0539: 'SafeAreaTests.DerivedDefaultSafeAreaContentPage.HasExplicitSafeAreaEdges' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(584,35): error CS0539: 'SafeAreaTests.DerivedDefaultSafeAreaContentPage.GetDefaultSafeAreaEdges()' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(597,26): error CS0539: 'SafeAreaTests.CustomNoneSafeAreaView.HasExplicitSafeAreaEdges' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(599,35): error CS0539: 'SafeAreaTests.CustomNoneSafeAreaView.GetDefaultSafeAreaEdges()' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(612,26): error CS0539: 'SafeAreaTests.CustomMixedSafeAreaView.HasExplicitSafeAreaEdges' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(614,35): error CS0539: 'SafeAreaTests.CustomMixedSafeAreaView.GetDefaultSafeAreaEdges()' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(497,43): error CS0535: 'SafeAreaTests.CustomSafeAreaView' does not implement interface member 'ISafeAreaElement.SafeAreaEdgesDefaultValueCreator()' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(524,43): error CS0535: 'SafeAreaTests.CustomSafeAreaPage' does not implement interface member 'ISafeAreaElement.SafeAreaEdgesDefaultValueCreator()' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(587,47): error CS0535: 'SafeAreaTests.CustomNoneSafeAreaView' does not implement interface member 'ISafeAreaElement.SafeAreaEdgesDefaultValueCreator()' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(602,48): error CS0535: 'SafeAreaTests.CustomMixedSafeAreaView' does not implement interface member 'ISafeAreaElement.SafeAreaEdgesDefaultValueCreator()' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
🟢 With fix — 🧪 SafeAreaTests: PASS ✅ · 15s

(no coded error found; showing last 1200 chars)

Default [< 1 ms]
  Passed SafeAreaEdgesTypeConverter_ConvertFromFourValues [< 1 ms]
  Passed IsSafeAreaEdgesSet_NullBindableThrows [< 1 ms]
  Passed SafeAreaEdgesTypeConverter_ConvertFromInvalidValue_ThrowsException [< 1 ms]
  Passed SafeAreaEdges_UniformConstructor_AppliesAllEdges [< 1 ms]
  Passed StackLayouts_RespectUserSettings [< 1 ms]
  Passed GetEdgeValue_TwoValues_AppliesCorrectly [< 1 ms]
  Passed CustomView_DefaultRegionsUseDeclaredEdges [< 1 ms]
  Passed GetEdgeValue_FourValues_AppliesCorrectly [< 1 ms]
  Passed SafeAreaEdgesTypeConverter_ConvertFromInvalidLength_ThrowsException [< 1 ms]
  Passed Layout_ImplementsISafeAreaView [< 1 ms]
  Passed SafeAreaEdges_AllEnumValues_WorkCorrectly [< 1 ms]
[xUnit.net 00:00:00.67]   Finished:    Microsoft.Maui.Controls.Core.UnitTests
  Passed CustomPage_CanOverrideInheritedSafeAreaStrategy [< 1 ms]
  Passed CustomView_CanReuseSafeAreaEdgesContract [< 1 ms]
  Passed StackLayout_HorizontalOrientation_RespectsDirectProperty_RTL [< 1 ms]
  Passed GetEdges_DefaultValue_ReturnsDefault [< 1 ms]
  Passed HasExplicitSafeAreaEdges_StyleValueCountsAsExplicit [1 ms]
Test Run Successful.
Total tests: 77
     Passed: 77
 Total time: 0.8697 Seconds
🔴 Without fix — 📄 SafeAreaEdgesTests: 🛠️ BUILD ERROR · 10s

Error-relevant lines (filtered from the build log):

/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/SafeAreaEdgesTests.xaml.cs(91,25): error CS0539: 'CustomSafeAreaElement.HasExplicitSafeAreaEdges' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/SafeAreaEdgesTests.xaml.cs(93,34): error CS0539: 'CustomSafeAreaElement.GetDefaultSafeAreaEdges()' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/SafeAreaEdgesTests.xaml.cs(81,52): error CS0535: 'CustomSafeAreaElement' does not implement interface member 'ISafeAreaElement.SafeAreaEdgesDefaultValueCreator()' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj]
🟢 With fix — 📄 SafeAreaEdgesTests: PASS ✅ · 19s

(no coded error found; showing last 1200 chars)

aui.Controls.Xaml.UnitTests
[xUnit.net 00:00:02.95]   Starting:    Microsoft.Maui.Controls.Xaml.UnitTests
  Passed FourValueConversions(inflator: SourceGen) [26 ms]
  Passed FourValueConversions(inflator: XamlC) [< 1 ms]
  Passed FourValueConversions(inflator: Runtime) [24 ms]
  Passed SingleValueConversions(inflator: XamlC) [< 1 ms]
  Passed SingleValueConversions(inflator: SourceGen) [< 1 ms]
  Passed SingleValueConversions(inflator: Runtime) [1 ms]
  Passed TwoValueConversions(inflator: XamlC) [< 1 ms]
  Passed TwoValueConversions(inflator: Runtime) [1 ms]
  Passed TwoValueConversions(inflator: SourceGen) [< 1 ms]
[xUnit.net 00:00:03.05]   Finished:    Microsoft.Maui.Controls.Xaml.UnitTests
  Passed PropertyInflation_WorksWithAllEnumValues(inflator: XamlC) [1 ms]
  Passed PropertyInflation_WorksWithAllEnumValues(inflator: Runtime) [1 ms]
  Passed PropertyInflation_WorksWithAllEnumValues(inflator: SourceGen) [< 1 ms]
  Passed ControlSpecificProperties(inflator: Runtime) [4 ms]
  Passed ControlSpecificProperties(inflator: XamlC) [< 1 ms]
  Passed ControlSpecificProperties(inflator: SourceGen) [< 1 ms]
Test Run Successful.
Total tests: 15
     Passed: 15
 Total time: 3.2648 Seconds
🔴 Without fix — 📄 Tests: 🛠️ BUILD ERROR · 7s

Error-relevant lines (filtered from the build log):

/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/SafeAreaEdgesTests.xaml.cs(91,25): error CS0539: 'CustomSafeAreaElement.HasExplicitSafeAreaEdges' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/SafeAreaEdgesTests.xaml.cs(93,34): error CS0539: 'CustomSafeAreaElement.GetDefaultSafeAreaEdges()' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/SafeAreaEdgesTests.xaml.cs(81,52): error CS0535: 'CustomSafeAreaElement' does not implement interface member 'ISafeAreaElement.SafeAreaEdgesDefaultValueCreator()' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj]
🟢 With fix — 📄 Tests: PASS ✅ · 120s

(no coded error found; showing last 1200 chars)

or(targetPlatformIdentifier: "") [1 s]
  Passed SingleProject_NonPlatformBuildExcludesPlatformSpecificFoldersButKeepsSharedFolder [1 s]
  Passed RandomEmbeddedResource [1 s]
  Passed SingleProject_RecognizedTfmIgnoresNeutralBackendSelector [1 s]
  Passed TargetsShouldSkip [2 s]
[xUnit.net 00:01:53.69]     TouchXamlFile [SKIP]
[xUnit.net 00:01:53.69]       source gen changes
  Skipped TouchXamlFile [1 ms]
  Passed ItemDisplayBindingWithoutDataTypeFails(inflator: XamlC) [87 ms]
  Passed ItemDisplayBindingWithoutDataTypeFails(inflator: SourceGen) [4 ms]
  Passed ItemDisplayBindingWithoutDataTypeFails(inflator: Runtime) [3 ms]
  Passed RequiredFieldsAndPropertiesAreSet(inflator: XamlC) [< 1 ms]
  Passed RequiredFieldsAndPropertiesAreSet(inflator: SourceGen) [17 ms]
  Passed RequiredFieldsAndPropertiesAreSet(inflator: Runtime) [< 1 ms]
  Passed ThrowsOnMismatchingType(inflator: SourceGen) [6 ms]
  Passed ThrowsOnMismatchingType(inflator: XamlC) [39 ms]
[xUnit.net 00:01:53.85]   Finished:    Microsoft.Maui.Controls.Xaml.UnitTests
  Passed ThrowsOnMismatchingType(inflator: Runtime) [< 1 ms]
Test Run Successful.
Total tests: 2123
     Passed: 2115
    Skipped: 8
 Total time: 1.9021 Minutes
🔴 Without fix — 📱 PageTests (ReadingDefaultSafeAreaEdgesPreservesLegacySafeAreaFallback): 🛠️ BUILD ERROR · 31s

Error-relevant lines (filtered from the build log):

/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Elements/View/ViewTests.cs(93,26): error CS0539: 'ViewTests.CustomSafeAreaView.HasExplicitSafeAreaEdges' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj::TargetFramework=net11.0-ios]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Elements/View/ViewTests.cs(95,35): error CS0539: 'ViewTests.CustomSafeAreaView.GetDefaultSafeAreaEdges()' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj::TargetFramework=net11.0-ios]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Elements/View/ViewTests.cs(73,43): error CS0535: 'ViewTests.CustomSafeAreaView' does not implement interface member 'ISafeAreaElement.SafeAreaEdgesDefaultValueCreator()' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj::TargetFramework=net11.0-ios]
Build FAILED.
🟢 With fix — 📱 PageTests (ReadingDefaultSafeAreaEdgesPreservesLegacySafeAreaFallback): ⚠️ ENV ERROR · 53s

No log file found

🔴 Without fix — 📱 ScrollViewHandlerTests (LegacySafeAreaViewWithoutModernContractRemainsEdgeToEdge): ⚠️ ENV ERROR · 51s

No log file found

🟢 With fix — 📱 ScrollViewHandlerTests (LegacySafeAreaViewWithoutModernContractRemainsEdgeToEdge): ⚠️ ENV ERROR · 52s

No log file found

⚠️ Failure Details (7 tests)
  • 🛠️ SafeAreaTests without fix: build failed before tests could run
    • /Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(519,26): error CS0539: 'SafeAreaTests.CustomSafeAreaView.HasExplicitSafeAreaEdges' in explicit interface declaration i...
  • 🛠️ SafeAreaEdgesTests without fix: build failed before tests could run
    • /Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/SafeAreaEdgesTests.xaml.cs(91,25): error CS0539: 'CustomSafeAreaElement.HasExplicitSafeAreaEdges' in explicit interface declaration is ...
  • 🛠️ Tests without fix: build failed before tests could run
    • /Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/SafeAreaEdgesTests.xaml.cs(91,25): error CS0539: 'CustomSafeAreaElement.HasExplicitSafeAreaEdges' in explicit interface declaration is ...
  • 🛠️ PageTests (ReadingDefaultSafeAreaEdgesPreservesLegacySafeAreaFallback) without fix: build failed before tests could run
    • /Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Elements/View/ViewTests.cs(93,26): error CS0539: 'ViewTests.CustomSafeAreaView.HasExplicitSafeAreaEdges' in explicit interface declaration...
  • ⚠️ ScrollViewHandlerTests (LegacySafeAreaViewWithoutModernContractRemainsEdgeToEdge) without fix: XHarness did not produce the expected fresh result 'testResults.xml' for requested class(es) 'Microsoft.Maui.DeviceTests.ScrollViewHandlerTests' (the target tests did not run).
  • ⚠️ PageTests (ReadingDefaultSafeAreaEdgesPreservesLegacySafeAreaFallback) with fix: XHarness did not produce the expected fresh result 'testResults.xml' for requested class(es) 'Microsoft.Maui.DeviceTests.PageTests' (the target tests did not run).
  • ⚠️ ScrollViewHandlerTests (LegacySafeAreaViewWithoutModernContractRemainsEdgeToEdge) with fix: XHarness did not produce the expected fresh result 'testResults.xml' for requested class(es) 'Microsoft.Maui.DeviceTests.ScrollViewHandlerTests' (the target tests did not run).
📁 Fix files reverted (43 files)
  • src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-blazor.aotprofile
  • src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-blazor.aotprofile.txt
  • src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-sc.aotprofile
  • src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-sc.aotprofile.txt
  • src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui.aotprofile
  • src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui.aotprofile.txt
  • src/Controls/src/Core/BindableObject.cs
  • src/Controls/src/Core/BindableProperty.cs
  • src/Controls/src/Core/Border/Border.cs
  • src/Controls/src/Core/ContentPage/ContentPage.cs
  • src/Controls/src/Core/ContentView/ContentView.cs
  • src/Controls/src/Core/Element/Element.cs
  • src/Controls/src/Core/InputView/InputView.cs
  • src/Controls/src/Core/Layout/Layout.cs
  • src/Controls/src/Core/Page/Page.cs
  • src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt
  • src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt
  • src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
  • src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
  • src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt
  • src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt
  • src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt
  • src/Controls/src/Core/SafeAreaElement.cs
  • src/Controls/src/Core/ScrollView/ScrollView.cs
  • src/Core/src/Core/ISafeAreaElement.cs
  • src/Core/src/Core/ISafeAreaView2.cs
  • src/Core/src/Handlers/View/ViewHandler.Android.cs
  • src/Core/src/Handlers/View/ViewHandler.cs
  • src/Core/src/Handlers/View/ViewHandler.iOS.cs
  • src/Core/src/Platform/Android/MauiWindowInsetListener.cs
  • src/Core/src/Platform/Android/SafeAreaExtensions.cs
  • src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs
  • src/Core/src/Platform/iOS/MauiScrollView.cs
  • src/Core/src/Platform/iOS/MauiView.cs
  • src/Core/src/Platform/iOS/SafeAreaPadding.cs
  • src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
  • src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt
  • src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
  • src/Core/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
  • src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt
  • src/Core/src/PublicAPI/net/PublicAPI.Unshipped.txt
  • src/Core/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt
  • src/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt

New files (not reverted):

  • src/Core/src/Core/ISafeAreaInsets.cs
  • src/Core/src/Core/ISafeAreaViewStrategy.cs

📋 Pre-Flight — Context & Validation

PR #37750 Pre-Flight

Context

  • Title: [net11.0] Expose safe area contract for custom views
  • Base: net11.0
  • Materialized review commit: d7905a8af8829a1a8da9fb8992aa9175551724ac
  • Related issue: #37384, which reports that custom views cannot participate in the .NET 10 per-edge SafeAreaEdges model or expose an effective strategy to native hosts.
  • Gate: Inconclusive because the existing test could not be built or run. This is not evidence that the PR fix fails, and gate verification must not be rerun in STEP 5a.

Current PR Approach

The PR replaces the internal numbered ISafeAreaView2 contract with a public ISafeAreaElement contract, adds explicit/default-value metadata and a public effective-strategy resolver, exposes reusable Controls bindable-property plumbing, and routes Apple/Android safe-area handling through a shared internal strategy resolver. It retains the shipped ISafeAreaView fallback and adds substantial unit, XAML, device, HostApp, AOT-profile, and platform behavior coverage.

The materialized diff changes 56 files (3020 insertions, 530 deletions), including:

  • public API and Controls property plumbing (ISafeAreaElement, SafeAreaElement, API baselines);
  • an internal compatibility resolver (ISafeAreaViewStrategy / SafeAreaViewStrategy);
  • iOS MauiView, MauiScrollView, keyboard, inset, and handler paths;
  • Android listener and inset paths;
  • built-in control defaults and bindable-property specificity behavior;
  • safe-area unit, XAML, Controls device, Core device, and AOT-profile coverage.

The diff adds ISafeAreaInsets.cs and ISafeAreaViewStrategy.cs and deletes ISafeAreaView2.cs; candidate attempts must obey the try-fix baseline allow-list and report Blocked if the baseline state contains any NewFiles.

Alternative-Fix Requirement

Each candidate must use a distinct root-cause hypothesis and must not reproduce the PR's shared public-contract-plus-central-resolver design. Candidate 2 must also avoid candidate 1's recorded mechanism.

Test Contract

Primary test:

pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "SafeAreaEdges"

Only after the primary test passes, run every regression command supplied by the STEP 5a request, including repeated entries. Any regression failure makes the candidate Fail. A missing simulator/device is Blocked; build, compile, or script errors are Fail.

Each attempt allows one implementation/test pass and at most one focused correction/retest. The gate test must not be rerun, no full suite may be run, and the exact restore command is:

pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore

Workspace Constraints

The checkout already contains numerous unrelated modified/untracked CI-script files. They predate STEP 5a and must not be edited, removed, restored, stashed, or included in candidate diffs. gate/content.md is owned by the prior gate phase and must not be created or overwritten.


🔬 Code Review — Deep Analysis

Expert PR Evaluation — dotnet/maui #37750

Scope reviewed: local raw submitted commit d7905a8af8829a1a8da9fb8992aa9175551724ac against base ec79089f65, in read-only worktree /Users/cloudtest/vss/_work/1/s. Authoritative diff = git diff ec79089f65..d7905a8af8. The public PR head may have advanced remotely; that newer head was not reviewed or fetched. Unrelated dirty .github/eng files in the worktree were ignored.

Method: independence-first. The full diff and the relevant full files at d7905a8af8 (plus callers/consumers and prior behaviour at ec79089f65) were read before any PR narrative was consulted.


1. What the PR actually does (independent reading)

This is a safe-area contract refactor that converts an internal, page-centric interface into a public, per-edge extensibility contract, plus several behavioural changes to iOS safe-area/keyboard geometry.

Structural changes:

Before After
internal interface ISafeAreaView2 (HasExplicitSafeAreaEdges, SafeAreaInsets setter, GetSafeAreaRegionsForEdge(int)) Deleted. Split three ways.
internal interface ISafeAreaElement (SafeAreaEdges, SafeAreaEdgesDefaultValueCreator()) Now public, with SafeAreaEdges, HasExplicitSafeAreaEdges, GetDefaultSafeAreaEdges().
New internal ISafeAreaInsets { Thickness SafeAreaInsets { set; } } (implemented only by Page).
New internal ISafeAreaViewStrategy { GetSafeAreaRegionsForEdge(int) } + internal static SafeAreaViewStrategy resolver.
internal static SafeAreaElement (Controls) Now public, exposing SafeAreaEdgesProperty and IsSafeAreaEdgesSet(BindableObject).
New public static SafeAreaElementExtensions.GetEffectiveSafeAreaEdges(this ISafeAreaElement).

Behavioural changes (not merely mechanical):

  1. ViewHandler.cs:81 — the SafeAreaEdges mapper registration moved from #if ANDROID || IOS to #if ANDROID || IOS || MACCATALYST. MACCATALYST is a distinct symbol from IOS in this repo (confirmed by the pervasive #if IOS || MACCATALYST idiom, e.g. ContentPage.cs), so the mapper was previously never registered on Mac Catalyst — runtime SafeAreaEdges changes did not propagate there at all. This is a genuine, previously-unfixed bug.
  2. Specificity-aware handler updatesBindableProperty.UpdateHandlerOnSpecificityChange (new internal flag, set only for SafeAreaEdgesProperty), plumbed through BindableObject.OnBindablePropertySet (new specificityChanged parameter) into Element.OnBindablePropertySet. Assigning the same value at a different specificity now updates the handler, because explicitness itself changes resolution.
  3. Ancestor suppression became per-edge and value-basedbool IsParentHandlingSafeArea()SafeAreaEdges GetParentHandledSafeAreaEdges(), which recomputes each ancestor's adjusted insets rather than reading a cached bool, and ExcludeParentHandledSafeAreaEdges zeroes only overlapping edges.
  4. Keyboard geometry rewrittenTryGetSoftInputBottomOverlap now converts the keyboard frame into window coordinates via window.ConvertRectFromCoordinateSpace(..., window.Screen.CoordinateSpace) and intersects against the view's own window-space frame (the old code compared _keyboardFrame against window.Frame and used Superview.ConvertRectToView(Frame, Window)). The !IsSoftInputHandledByParent(this) gate was removed and replaced by per-view frame-relative overlap plus bottomIncludesKeyboardOverlap.
  5. New subtree invalidationMauiView.InvalidateSafeArea(UIView) (static, recursive over all subviews) and InvalidateDescendantSafeAreas(), fired from keyboard callbacks and from MapSafeAreaEdges.

2. Independent assessment

The contract split is right. The old ISafeAreaView2 conflated three unrelated concerns — a per-edge resolution strategy, an inset write-back sink, and a public configuration surface — and forced every implementer (Border, ContentView, Layout, ScrollView, Page) to stub out the parts it did not need (Thickness ISafeAreaView2.SafeAreaInsets { set { } } appears four times in the deleted code). Splitting into ISafeAreaElement (public config) / ISafeAreaViewStrategy (internal resolution) / ISafeAreaInsets (internal sink) is correct layer placement, and centralising resolution in SafeAreaViewStrategy removes five near-duplicate GetSafeAreaRegionsForEdge implementations. The new types correctly live in src/Core so ISafeAreaElement is referenceable without adding Core interface deps to Controls.csproj — this matches the guidance in safe-area-ios.instructions.md.

Ordering in SafeAreaViewStrategy.TryGetSafeAreaEdges is ISafeAreaViewStrategyISafeAreaElement → (optionally) legacy ISafeAreaView, which preserves built-in compatibility behaviour while letting custom views opt into the modern contract. That is consistent across all four call sites, and includeLegacy: false is passed everywhere a legacy fallback would have changed existing behaviour.

The mechanical parts I verified as behaviour-preserving:

  • SafeAreaPadding is (Left, Right, Top, Bottom); every new construction site passes them in that order (ExcludeParentHandledSafeAreaEdges, GetAdjustedSafeAreaInsets). SafeAreaEdges is (Left, Top, Right, Bottom); every new construction site matches. No positional-argument transposition.
  • SetterSpecificity defines operator != (SetterSpecificity.cs:248), and originalSpecificity is captured before context.Values.SetValue(...) in SetValueActual, so specificityChanged is computed correctly. SetterSpecificityList.GetSpecificity() is O(1) (_top.Specificity), so the new per-set computation is negligible.
  • MauiWindowInsetListener.HasExplicitSafeAreaEdges and SafeAreaExtensions.ApplyAdjustedSafeAreaInsetsPx retain equivalent type coverage after the swap (Page implements ISafeAreaViewStrategy; ContentPage implements ISafeAreaElement; both resolve to the same values as before).
  • MauiScrollView is a UIScrollView, not a MauiView, so the if (MauiView) … else if (MauiScrollView) dispatch in InvalidateSafeArea is correct, not an accidental exclusive branch.
  • ViewHandler.iOS.MapSafeAreaEdges uses handler.PlatformView is PlatformView platformView, which null-guards via the pattern; no NRE risk there.

Where I disagree with the change as submitted is concentrated in the iOS runtime behaviour, not the contract. Three items are load-bearing and, in my reading, regressions or unproven assumptions; the rest are cost/documentation concerns. Details in §3.


3. Findings with evidence

3.1 [major]MauiScrollView lost ancestor suppression on the SystemAdjustedContentInset branch

MauiScrollView.cs:389:

_appliesSafeAreaAdjustments = RespondsToSafeArea() && !_safeArea.IsEmpty;

The !IsParentHandlingSafeArea() term was removed. Its replacement, ExcludeParentHandledSafeAreaEdges(_safeArea, GetParentHandledSafeAreaEdges()), is applied only inside the first branch (SystemAdjustedContentInset == UIEdgeInsets.Zero || ContentInsetAdjustmentBehavior == Never). The else branch is:

else
{
    // UIKit's adjusted inset is authoritative once the scroll view is scrollable.
    // Filtering it through MAUI ancestor padding would discard native scroll insets.
    _safeArea = SystemAdjustedContentInset.ToSafeAreaInsets();
}

No ancestor filtering, and _appliesSafeAreaAdjustments no longer gates on ancestors either. So for a scrollable ScrollView under a safe-area-applying MauiView ancestor, both now inset the same edges. Prior to this commit IsParentHandlingSafeArea() forced _appliesSafeAreaAdjustments = false in exactly that configuration.

Repro shape: ContentPage SafeAreaEdges="All"VerticalStackLayoutScrollView with content taller than the viewport (behavior stays Automatic, SystemAdjustedContentInset != Zero). This is the double-apply/oscillation family tracked by #33595 and #32586, and the comment justifying the carve-out ("UIKit's adjusted inset is authoritative") explains why UIKit's value should not be filtered, but does not address why the scroll view should still apply it when an ancestor already did.

Coverage: ScrollViewHandlerTests.iOS.cs adds exactly one test, LegacySafeAreaViewWithoutModernContractRemainsEdgeToEdge, which asserts ContentInsetAdjustmentBehavior == Never — i.e. it exercises the other branch. The regressed path has no test.

3.2 [major]bottomIncludesKeyboardOverlap can permanently double keyboard padding

MauiView.cs:592:

!bottomIncludesKeyboardOverlap && parentHandledEdges.Bottom != SafeAreaRegions.None ? 0 : safeArea.Bottom

with the comment "Keep a child's positive frame-relative keyboard overlap until its parent arranges that child above the keyboard; the overlap is then zero and normal suppression applies."

That convergence argument holds only when the parent's bottom inset actually moves the child. It does not for a child that is bottom-aligned in a fixed-height row, absolutely positioned, or otherwise not repositioned by the parent's content-rect shrink. In those cases TryGetSoftInputBottomOverlap keeps returning a positive overlap indefinitely and both ancestor and descendant pad for the keyboard — roughly 2× keyboard height. The removed !IsSoftInputHandledByParent(this) gate is precisely what previously prevented this.

IsSoftInputHandledByParent still exists but is now only consumed by KeyboardAutoManagerScroll.AdjustPositionDebounce (KeyboardAutoManagerScroll.cs:309) — it no longer participates in inset computation at all.

3.3 [major] — layout-pass amplification and non-convergence risk

MauiView.cs:863 calls InvalidateDescendantSafeAreas() from inside ValidateSafeArea(), which runs on the layout path. Each descendant gets SetNeedsLayout() during a layout pass, and each descendant that changes then invalidates its own descendants.

Combined with §3.2's deliberate two-phase convergence, a single keyboard show on nested SoftInput edges costs a minimum of three passes (parent pads → child overlap resolves to 0 → parent re-measures). The EqualsAtPixelLevel guard in the return value (MauiView.cs:~866) terminates the loop only when values converge; it does not prevent a feedback cycle where the child's applied inset feeds back into the parent's arrange.

Related cost, same mechanism:

  • MauiView.cs:554GetParentHandledSafeAreaEdges now calls mauiView.GetAdjustedSafeAreaInsets(...) per ancestor (interface type tests + virtual GetDefaultSafeAreaEdges() + struct construction + a 4-edge scan, plus ConvertRectToView/ConvertRectFromCoordinateSpace for the nearest ancestor). Previously: read a cached bool field. O(views × depth), and the _parentHandledSafeAreaEdges cache is nulled for every descendant on each change.
  • MauiView.cs:976InvalidateSafeArea(UIView) recurses the entire native subtree with no early exit; each node's Subviews access marshals a fresh managed array. Fires on every keyboard show/hide/frame-change and on every MapSafeAreaEdges.
  • ViewHandler.iOS.cs:167 — that full-subtree walk is now also triggered by specificity-only changes (via UpdateHandlerOnSpecificityChange), e.g. a Style re-applying the same SafeAreaEdges value or a VisualState re-entry.

Per repo convention, cache/hot-path changes of this shape need dotnet-trace/speedscope evidence; none is present in the diff.

3.4 [moderate] — ancestor region semantics are collapsed to a marker

MauiView.cs:567 and neighbours set left/top/right/bottom = SafeAreaRegions.Container purely as a "handled" marker, discarding the ancestor's actual region. A SoftInput-bottom ancestor (transient, keyboard-driven) therefore suppresses a Container-bottom child (persistent, home-indicator) for the duration of the keyboard session. Recording the ancestor's resolved region via SafeAreaViewStrategy.GetSafeAreaRegionsForEdge would let suppression apply only to genuinely overlapping semantics.

3.5 [moderate]HasSoftInputBottomOverlapChanged() is a mutating predicate on the fast path

MauiView.cs:407. It mutates _lastSoftInputBottomOverlap, is non-idempotent within a pass, and runs before the !_safeAreaInvalidated early-return — so the steady-state "nothing changed" path now always pays a TryGetSafeAreaEdges call and, with the keyboard up, two coordinate conversions per layout.

3.6 [moderate] — bindable-property specificity coverage gap

Element.cs:711. The forward direction (setting a value at a new specificity) is covered. The reverse — ClearValue/style-unapply causing HasExplicitSafeAreaEdges to flip back to false — depends on the new original.Key != bpcontext.Values.GetSpecificity() check at BindableObject.cs:152, and SafeAreaTests.cs (+517 lines) contains no test exercising it. Note also that SetValueActual's if (specificity < originalSpecificity) branch returns without calling OnBindablePropertySet at all (pre-existing), so the specificity-change signal is deliberately partial.

3.7 [moderate] / [minor] — public-surface polish

  • SafeAreaElementExtensions.GetEffectiveSafeAreaEdges (ISafeAreaElement.cs:61) is documented as returning "the per-edge safe area strategy consumed by MAUI platform handlers", but handlers layer keyboard state and ancestor suppression on top of that value, so it does not describe what is applied. It also has no in-repo consumer.
  • SafeAreaElement.IsSafeAreaEdgesSet (SafeAreaElement.cs:57) silently returns false for a BindableObject that declares its own SafeAreaEdges property rather than reusing the shared instance — a silent degradation in a newly public extensibility contract.
  • Page.cs:268–271's new this is ISafeAreaElement branch is unreachable for every built-in Page subclass (ContentPage re-implements the interface explicitly; all others never implement ISafeAreaElement). It exists only for user subclasses and has no test.

4. Public API and compatibility assessment

Additions are correctly declared. PublicAPI.Unshipped.txt was updated for all 7 Controls RIDs and all 8 Core RIDs. Core entries:

Microsoft.Maui.ISafeAreaElement
Microsoft.Maui.ISafeAreaElement.SafeAreaEdges.get -> Microsoft.Maui.SafeAreaEdges
Microsoft.Maui.ISafeAreaElement.HasExplicitSafeAreaEdges.get -> bool
Microsoft.Maui.ISafeAreaElement.GetDefaultSafeAreaEdges() -> Microsoft.Maui.SafeAreaEdges
Microsoft.Maui.SafeAreaElementExtensions
static Microsoft.Maui.SafeAreaElementExtensions.GetEffectiveSafeAreaEdges(...) -> Microsoft.Maui.SafeAreaEdges

Controls entries add SafeAreaElement, SafeAreaElement.IsSafeAreaEdgesSet, SafeAreaElement.SafeAreaEdgesProperty. These match the actual shapes. PublicAPI.Shipped.txt was not touched — correct.

No breaking change to shipped API. ISafeAreaView2 and the old ISafeAreaElement were both internal, so their deletion/reshaping is invisible to consumers. SafeAreaEdgesDefaultValueCreator()GetDefaultSafeAreaEdges() is an internal-to-public rename with no shipped predecessor. The public instance SafeAreaEdges properties on Border/ContentView/Layout/ScrollView/ContentPage already existed at the base commit and are unchanged.

Forward-compatibility risk to flag for the API council, not a blocker: ISafeAreaElement is now a public interface with three members. Adding a member to it later is a breaking change (default interface method / IFoo2 / extension method would be required). Given that the design explicitly justifies why all three members are needed, this is defensible — but SafeAreaElementExtensions.GetEffectiveSafeAreaEdges (§3.7) is the piece I would not ship without a demonstrated consumer, since public static extensions on a public contract cannot be withdrawn.

Behavioural compatibility: the Mac Catalyst mapper registration change (#if ANDROID || IOS+ || MACCATALYST) is a behaviour change on Mac Catalyst — runtime SafeAreaEdges mutations now take effect where they previously silently did nothing. That is the intended fix, but Mac Catalyst apps that inadvertently depended on the property being inert will see layout shifts. Worth a release note. Mac Catalyst also defaults UseSafeArea to true (unlike iOS), and ContentPage's new explicit ISafeAreaElement.SafeAreaEdges getter routes the unset case through ((ISafeAreaView)this).IgnoreSafeArea under #if IOS || MACCATALYST, which preserves that asymmetry correctly.

Default-value semantics for third parties: a custom view implementing only ISafeAreaElement and returning SafeAreaEdges.Default from GetDefaultSafeAreaEdges() resolves to Container (SafeAreaViewStrategy.ResolveDefaultRegion), which differs from every built-in control's default (Border/ContentView/ContentPage = None, Layout = Container, ScrollView = unresolved Default). This is documented in the interface remarks and appears deliberate, but it is a divergence third-party authors will hit.


5. Blast radius

High. This is not a localised fix.

  • Types whose interface list changed: Border, ContentPage, ContentView, Layout (base of every layout panel), Page (base of every page), ScrollView. Layout and Page sit under essentially all MAUI content.
  • Framework-wide plumbing: BindableObject.OnBindablePropertySet gained a parameter, overridden in Element and InputView. Element.OnBindablePropertySet runs for every bindable property set on every element — the specificityChanged computation is now unconditional there. It is O(1), but the code path is the single hottest one in Controls.
  • iOS/Mac Catalyst layout core: MauiView (+354/-… lines) and MauiScrollView are the base platform views for nearly all MAUI content on Apple platforms. The safe-area/keyboard changes affect every page, every scroll view, and every CollectionView cell backed by a MauiView.
  • Android: ViewHandler.Android.cs gained an IsModernSafeAreaView early-return in MapSafeAreaEdges, and SafeAreaExtensions/MauiWindowInsetListener were re-pointed at the new resolver. Coverage looks equivalent, but the inset pipeline is shared by all Android views.
  • Platforms not exercised by the new tests: Windows and Tizen get new PublicAPI.Unshipped.txt entries (the interfaces are in the shared netstandard/net surface) but no behavioural coverage — acceptable, since the mapper is #if ANDROID || IOS || MACCATALYST.
  • AOT/trimming: no reflection, no Type.GetType/Activator.CreateInstance, no new suppressions or DynamicallyAccessedMembers. SafeAreaViewStrategy resolves via plain is-pattern type tests, which are trimmer- and AOT-safe. The .aotprofile binaries were regenerated (small deltas, consistent with the removed ISafeAreaView2 dispatch). No AOT/trimming concerns.
  • XAML: SafeAreaEdgesTests.xaml/.xaml.cs gained coverage; the shared SafeAreaEdgesProperty is still surfaced per-type through each control's own public static field, so XAML addressability is preserved (the interface remarks explicitly call this out).

6. Hard failure-mode probes

# Probe Result
P1 Positional-arg transposition in the new SafeAreaPadding(L,R,T,B) / SafeAreaEdges(L,T,R,B) construction sites Pass — all four new sites match their declared orders; UIEdgeInsets(top,left,bottom,right) in GetInset also correct.
P2 Does #if ANDROID || IOS || MACCATALYST change anything, or is IOS already implied on Mac Catalyst? Real change#if IOS || MACCATALYST is used throughout this repo, proving MACCATALYST is disjoint. Previously-dead mapper on Mac Catalyst is now live.
P3 SetterSpecificity != operator exists; originalSpecificity captured before mutation PassSetterSpecificity.cs:248; capture precedes context.Values.SetValue in SetValueActual.
P4 Is GetSpecificity() O(1)? (new call on the universal BP-set path) PassSetterSpecificityList.GetSpecificity() returns _top.Specificity.
P5 Guard made more restrictive — IsModernSafeAreaView early-return in MapSafeAreaEdges (both platforms). What previously-passing input does it now reject? Intentional — rejects views with a SafeAreaEdges-named property that implement neither ISafeAreaViewStrategy nor ISafeAreaElement. No such built-in exists; the new LegacySafeAreaViewWithoutModernContractRemainsEdgeToEdge test pins the legacy-only case.
P6 Does a legacy-only ISafeAreaView still resolve? includeLegacy: false is passed at every new call site. Pass, by design — inset computation deliberately excludes legacy; legacy is preserved via ContentPage's compatibility getter and Page's strategy.
P7 Ancestor suppression correctness when an intermediate ancestor's own inset is itself suppressed PassGetParentHandledSafeAreaEdges reads the ancestor's raw adjusted insets, so an edge applied by a grandparent is still seen as handled by the intermediate; net effect is applied once.
P8 MauiView vs MauiScrollView dispatch in InvalidateSafeArea — is the else if accidentally exclusive? PassMauiScrollView : UIScrollView, not a MauiView; branches are genuinely disjoint.
P9 Upward layout loop from InvalidateDescendantSafeAreas Downward-only, terminates in isolation — but see §3.3 for the parent↔child keyboard feedback path that is not bounded by construction.
P10 ExcludeParentHandledSafeAreaEdges reachable on the SystemAdjustedContentInset branch of MauiScrollView Fail — unreachable; see §3.1.
P11 New negative-case test coverage for the new guards Partial — P5's negative case is covered by the new stub test; §3.1's and §3.6's negative cases are not.
P12 Input/path correctness (dimension 31) — external values reaching file/process/parser/navigation sinks N/A — no such surface in this diff; no credentials, archives, paths, or URI parsing touched.

7. Test and gate evidence status

Tests added (substantial, and largely well-targeted):

File Δ
Core.UnitTests/SafeAreaTests.cs +517/−… (rewritten)
DeviceTests/Elements/View/ViewTests.iOS.cs +1544
DeviceTests/Elements/View/ViewTests.Android.cs +173
DeviceTests/Elements/View/ViewTests.cs +35
DeviceTests/Elements/Page/PageTests.iOS.cs +18
Xaml.UnitTests/SafeAreaEdgesTests.xaml{,.cs} +20
Core/tests/DeviceTests/.../ScrollViewHandlerTests.iOS.cs +21
DeviceTests/CollectionView/CollectionViewTests.Android.cs +14/−…

Test types are placed in the right projects (XAML tests in Xaml.UnitTests, handler tests in Core/tests/DeviceTests, control tests in Controls/tests/DeviceTests), and iOS device tests compile for Mac Catalyst, which gives incidental coverage of the P2 fix.

Identified coverage gaps (each maps to a finding):

  1. Scrollable ScrollView nested under a safe-area-applying ancestor — the branch regressed in §3.1. The one new ScrollViewHandlerTests.iOS.cs test covers the opposite branch.
  2. Nested SoftInput bottom where the parent's inset does not reposition the child (§3.2).
  3. Layout-pass count / non-oscillation assertion for a keyboard show over nested SoftInput edges (§3.3).
  4. ClearValue/style-unapply on SafeAreaEdgesProperty flipping HasExplicitSafeAreaEdges back to false (§3.6).
  5. A custom Page subclass implementing ISafeAreaElement, exercising the new Page.cs:268 branch (§3.7).
  6. No dotnet-trace/speedscope evidence for the hot-path changes in §3.3, which repo convention requires for cache replacement.

Gate: the supplied gate is INCONCLUSIVE due to a build/environment error. Per the review instruction, this is explicitly not treated as a failing verification and does not by itself drive the verdict. Consequently I have no compile or test-execution evidence for this commit and this review is static-analysis-only; every finding above is derived from reading the diff, the full files at d7905a8af8, and the prior behaviour at ec79089f65. The claims most sensitive to that limitation are §3.2 and §3.3, whose failure modes are geometric and would be confirmed or refuted quickly on a device.


8. Verdict

NEEDS_CHANGES

Rationale. The architectural core of this PR — splitting ISafeAreaView2 into ISafeAreaElement / ISafeAreaViewStrategy / ISafeAreaInsets, centralising resolution in SafeAreaViewStrategy, and making the configuration contract public — is well-designed, correctly layered, properly declared in PublicAPI.Unshipped.txt, AOT/trim-clean, and backed by a genuinely large test addition. The Mac Catalyst mapper registration is a real bug fix. I would be comfortable with all of that.

The verdict is driven by one concrete regression and one unproven convergence assumption, both in iOS runtime geometry, both in code paths that sit under essentially every MAUI page:

  1. §3.1 (blocking)MauiScrollView.cs:389 drops ancestor suppression on the SystemAdjustedContentInset branch, where the replacement ExcludeParentHandledSafeAreaEdges call is unreachable. This re-opens the double-apply configuration behind #33595/#32586 for the most common scroll case (content taller than viewport), and the single new scroll-view test covers the other branch. This needs either the exclusion applied to that branch or the gate restored, plus a regression test named against the issue.
  2. §3.2 (blocking) — removing !IsSoftInputHandledByParent(this) from inset computation in favour of bottomIncludesKeyboardOverlap is only self-correcting when the parent's inset repositions the child. A bottom-aligned or absolutely-positioned SoftInput child yields permanent double keyboard padding. This needs either a bound on the fallback or a device test proving convergence for a child the parent does not move.
  3. §3.3 (needs evidence, not necessarily a code change) — the ancestor walk changed from reading a cached bool to recomputing each ancestor's full adjusted insets, and safe-area invalidation now recurses whole native subtrees on every keyboard event and every MapSafeAreaEdges (including specificity-only no-op updates). Per repo convention for cache replacement on a hot path, this needs dotnet-trace numbers before merge.

§3.4–§3.7 are non-blocking and can be addressed in follow-up, with one exception I would like resolved before merge because it cannot be withdrawn later: SafeAreaElementExtensions.GetEffectiveSafeAreaEdges should either have its documentation corrected or be dropped, since it is a new public static on a new public interface with no in-repo consumer and a doc comment that overstates what it returns.

Because the gate is inconclusive rather than red, none of the above rests on a failed build; all of it rests on the diff itself. If §3.1 and §3.2 are shown by device testing to be non-issues (i.e. the else branch is genuinely unreachable in practice for nested scroll views, and the overlap always converges), this moves to LGTM pending only the §3.3 trace and the §3.7 API note.

Confidence: high on §3.1 (the unreachable-guard argument is purely structural and does not depend on runtime behaviour); medium-high on §3.2 and §3.3 (the failure geometry is clear but unverified on device, as the gate could not run).


🛠️ Try-Fix — Analysis & Comparison

PR #37750 — STEP 5a Try-Fix Aggregate

Candidate 1 — Handler-Mapped Attached Safe-Area State

Model: claude-opus-5
Result: Blocked
Files changed: None
Self-review: 0 findings
Candidate narrative: ../try-fix-1/content.md
Attempt artifacts: attempt-1/

Approach

Expose safe-area configuration as an attached Controls property on any VisualElement, carry it through the existing ViewHandler property mapper, and push the resolved four-edge value onto MauiView/MauiScrollView as host-owned state. Keep the shipped ISafeAreaView fallback unchanged.

This avoids PR #37750's public ISafeAreaElement plus centralized strategy-resolver design. The PR fixes an unrecognized custom view by broadening a host-side type-identity pull; candidate 1 instead removes the recognition step. A handler mapper pushes data to the native host, so participation depends on the existing handler pipeline rather than implementation of a new MAUI interface.

Result and Failure Analysis

The approach was designed but not implemented. EstablishBrokenBaseline.ps1 rejected the pre-existing dirty worktree before creating .github/.baseline-state.json, so no RevertedFiles edit allow-list existed. The unrelated .github/scripts, .github/skills, and eng/scripts changes are off-limits and cannot be cleaned by an attempt. Independently, the PR adds ISafeAreaInsets.cs and ISafeAreaViewStrategy.cs; a successfully generated baseline would therefore contain NewFiles, which also requires Blocked.

The candidate diff is empty. The primary SafeAreaEdges HostApp command was skipped because no fix could be applied. All 13 supplied regression commands were consequently skipped because they are gated on primary-test success. The required restore command ran and reported the expected no-state result: No baseline state found / Restored False; no attempt-created changes existed.

Candidate 2 must avoid the attached-property, handler-mapper push, and native-host-owned strategy mechanism.

Candidate 2 — Version Existing ISafeAreaView

Model: gpt-5.6-sol
Result: Blocked
Files changed: None
Self-review: 0 findings
Candidate narrative: ../try-fix-2/content.md
Attempt artifacts: attempt-2/

Approach

Extend the already-public, already-recognized ISafeAreaView with a getter-only per-edge strategy and default interface behavior. Existing implementations would map IgnoreSafeArea to all-None or all-Container; custom views could override the per-edge member through a concrete bindable/XAML property. Apple hosts would continue pulling through the existing contract identity.

This differs from both earlier mechanisms. It neither introduces PR #37750's second public contract and centralized compatibility resolver nor uses candidate 1's attached property and handler-mapper push. It addresses missing expressiveness by versioning the public contract native hosts already recognize, with default interface members providing legacy compatibility.

Result and Failure Analysis

The approach was designed but not implemented. Baseline setup again rejected the pre-existing dirty tracked CI/script files and left .github/.baseline-state.json absent, so there was no RevertedFiles edit allow-list. The PR's added production files would independently force Blocked through the NewFiles rule.

The candidate diff is empty. The primary SafeAreaEdges HostApp command was skipped because no implementation could legally be applied. All 13 supplied regression commands were skipped because the primary did not pass. The required restore command ran and reported the expected no-state result: No baseline state found / Restored False; no attempt-created changes existed.

Aggregate Outcome

Two distinct alternative mechanisms were produced, satisfying the two-candidate cap, but neither could be implemented or empirically evaluated under the enforced baseline boundary. Both results are Blocked, not Fail: no compile, runtime, simulator, primary-test, or regression-test evidence was obtained. STEP 5b should assess the current PR code on its merits without treating either blocked candidate or the inconclusive gate as evidence against it.


🏁 Report — Final Recommendation

⚠️ Final Recommendation: REQUEST CHANGES

Winner: pr

The raw submitted PR is the comparative winner because it is the only implemented candidate with a passing required regression record: all 13 issue-specific iOS regressions passed. This is not an approval. The trusted Gate remains inconclusive, the expert evaluation identified unresolved Apple-platform correctness/performance concerns, and the functionally equivalent pr-plus-reviewer runtime path failed one focused SafeAreaEdges test.

Comparative ranking

Rank Candidate Implementation Validation evidence Assessment
1 pr Complete submitted 56-file public-contract and platform resolver implementation Gate inconclusive; 13/13 required issue regressions passed Best available evidence and therefore the winner, but not ready for approval while expert concerns and the runtime-change failure remain unresolved.
2 pr-plus-reviewer Raw PR plus corrected public docs, explicit mutating-helper naming, and ClearValue specificity coverage HostApp built; 11/12 SafeAreaEdges tests passed, with VerifyRuntimeSafeAreaEdgesChange timing out Improves API clarity and coverage, but ranks below the regression-passing raw PR because its required targeted validation failed. No retry was performed.
3 try-fix-1 Proposed attached SafeArea.Edges state pushed through the handler mapper Blocked before implementation; empty diff; no tests Design-only candidate with no empirical evidence.
4 try-fix-2 Proposed versioning of the existing ISafeAreaView contract Blocked before implementation; empty diff; no tests Design-only candidate with no empirical evidence.

Expert review reconciliation

The expert pass correctly identified three low-risk improvements: the public GetEffectiveSafeAreaEdges() documentation described final handler behavior too strongly, the keyboard-overlap helper name hid mutation, and the reverse ClearValue specificity transition lacked a focused test. These are captured in pr-plus-reviewer/reviewer.patch.

Two proposed runtime reversions were not safe to apply in the single refinement:

  • SystemAdjustedScrollViewInsetsAreNotSuppressedByParent explicitly establishes that UIKit-adjusted scroll insets remain authoritative when an ancestor has a safe area. Applying ancestor filtering to that branch would reverse submitted behavior without a reproducer.
  • ParentAndChildKeyboardSafeAreasProtectOverflowingChild explicitly requires a non-repositioned child to retain its frame-relative keyboard overlap, while ParentAndChildKeyboardSafeAreasDoNotDoublePadArrangedChild verifies suppression after the child is repositioned. Restoring an all-or-nothing parent gate would regress the overflowing-child case.

The expert's hot-path concern remains unresolved: recursive descendant invalidation and per-ancestor adjusted-inset resolution have broad layout cost, and no trace comparison is available. More importantly, the one-shot candidate validation exposed a direct runtime failure in VerifyRuntimeSafeAreaEdgesChange; because the candidate's runtime code differs only by a private rename, that result creates uncertainty about the submitted behavior rather than demonstrating a reviewer-patch regression.

Required disposition

Request changes to resolve or explain the VerifyRuntimeSafeAreaEdgesChange failure and provide evidence for the unresolved Apple safe-area propagation/performance concerns. The inconclusive Gate is recorded as uncertainty and is not, by itself, the reason for this recommendation.


🔗 Regression Cross-Reference

🔍 Regression Cross-Reference

Revert risks detected — this PR removes 3 line(s) previously added by labeled bug-fix PRs.

File Fix PR Fixed issue(s) Risk Reverted line
src/Core/src/Platform/iOS/MauiScrollView.cs #34024 #32586, #33934, #33595, #34042 ✗ REVERT bool? _parentHandlesSafeArea;
src/Core/src/Platform/iOS/MauiView.cs #34024 #32586, #33934, #33595, #34042 ✗ REVERT bool? _parentHandlesSafeArea;
src/Core/src/Platform/iOS/SafeAreaPadding.cs #34024 #32586, #33934, #33595, #34042 ✗ REVERT return RoundToPixel(Left, scale) == RoundToPixel(other.Left, scale)

Action required: Verify that issues #32586, #33595, #33934, #34042 do not re-regress before merging.

🧪 Regression Tests to Verify

These tests were added by the fix PRs being reverted. They must still pass:

Fix PR Type Test Filter
#34024 UITest Issue28986_ParentChildTest Issue28986_ParentChildTest
#34024 UITest Issue32586 Issue32586
#34024 UITest Issue33595 Issue33595
#34024 UITest Issue33934 Issue33934
#34024 UITest Issue28986_ParentChildTest Issue28986_ParentChildTest
#34024 UITest Issue32586 Issue32586
#34024 UITest Issue33595 Issue33595
#34024 UITest Issue33934 Issue33934
#34024 UITest Issue28986_ParentChildTest Issue28986_ParentChildTest
#34024 UITest Issue32586 Issue32586
#34024 UITest Issue33595 Issue33595
#34024 UITest Issue33934 Issue33934

🧪 Regression Test Results

PASSED — 13 passed, 0 failed, 0 skipped

Fix PR Test Type Result
#35916 Issue35756 UITest ✅ PASSED
#34024 Issue28986_ParentChildTest UITest ✅ PASSED
#34024 Issue32586 UITest ✅ PASSED
#34024 Issue33595 UITest ✅ PASSED
#34024 Issue33934 UITest ✅ PASSED
#34024 Issue28986_ParentChildTest UITest ✅ PASSED
#34024 Issue32586 UITest ✅ PASSED
#34024 Issue33595 UITest ✅ PASSED
#34024 Issue33934 UITest ✅ PASSED
#34024 Issue28986_ParentChildTest UITest ✅ PASSED
#34024 Issue32586 UITest ✅ PASSED
#34024 Issue33595 UITest ✅ PASSED
#34024 Issue33934 UITest ✅ PASSED

📱 UI Tests — Border,Layout,Page,SafeAreaEdges,ScrollView,ViewBaseTests

Detected UI test categories: Border,Layout,Page,SafeAreaEdges,ScrollView,ViewBaseTests

Deep UI tests — 661 passed, 0 failed, 6 skipped across 6 categories on platform-pool agent (replaces in-process counts above).

🧪 UI Test Execution Results (deep, platform pool)

Category Tests Snapshot diffs
Border 58/58 ✓
Layout 194/199 (5 skipped) ✓
Page 26/26 ✓
SafeAreaEdges 109/109 ✓
ScrollView 162/163 (1 skipped) ✓
ViewBaseTests 112/112 ✓
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs)

🧭 Next Steps — review latest findings

No alternative fix was selected for this run. Review the session findings and CI results before merging.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Aug 27, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
@kubaflo

kubaflo commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

@MauiBot addressed the actionable review follow-ups in 01770ad: added mutation-proven ClearValue explicitness coverage and clarified that the public resolver returns handler input rather than final applied insets. I also replied with existing device/mutation evidence for the intentional safe-area invariants. This is ready for re-review — thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-safearea Issues/PRs that have to do with the SafeArea functionality p/0 Current heighest priority issues that we are targeting for a release. platform/ios s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants